/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.catalina.valves;

import org.apache.catalina.Context;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.apache.juli.logging.Log;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.regex.Pattern;

/**
 * Implementation of a Valve that performs filtering based on comparing the
 * appropriate request property (selected based on which subclass you choose
 * to configure into your Container's pipeline) against the regular expressions
 * configured for this Valve.
 * <p>
 * This valve is configured by setting the <code>allow</code> and/or
 * <code>deny</code> properties to a regular expressions (in the syntax
 * supported by {@link Pattern}) to which the appropriate request property will
 * be compared. Evaluation proceeds as follows:
 * <ul>
 * <li>The subclass extracts the request property to be filtered, and
 * calls the common <code>process()</code> method.
 * <li>If there is a deny expression configured, the property will be compared
 * to the expression. If a match is found, this request will be rejected
 * with a "Forbidden" HTTP response.</li>
 * <li>If there is a allow expression configured, the property will be compared
 * to each such expression.  If a match is found, this request will be
 * allowed to pass through to the next Valve in the current pipeline.</li>
 * <li>If a deny expression was specified but no allow expression, allow this
 * request to pass through (because none of the deny expressions matched
 * it).
 * <li>The request will be rejected with a "Forbidden" HTTP response.</li>
 * </ul>
 * <p>
 * As an option the valve can generate an invalid <code>authenticate</code>
 * header instead of denying the request. This can be combined with the
 * context attribute <code>preemptiveAuthentication="true"</code> and an
 * authenticator to force authentication instead of denial.
 * <p>
 * This Valve may be attached to any Container, depending on the granularity
 * of the filtering you wish to perform.
 *
 * @author Craig R. McClanahan
 */
public abstract class RequestFilterValve extends ValveBase {

	/**
	 * The descriptive information related to this implementation.
	 */
	private static final String info =
			"org.apache.catalina.valves.RequestFilterValve/1.0";

	// ----------------------------------------------------- Class Variables
	/**
	 * The regular expression used to test for allowed requests.
	 */
	protected volatile Pattern allow = null;


	// ----------------------------------------------------- Instance Variables
	/**
	 * The current allow configuration value that may or may not compile into a
	 * valid {@link Pattern}.
	 */
	protected volatile String allowValue = null;
	/**
	 * Helper variable to catch configuration errors.
	 * It is <code>true</code> by default, but becomes <code>false</code>
	 * if there was an attempt to assign an invalid value to the
	 * <code>allow</code> pattern.
	 */
	protected volatile boolean allowValid = true;
	/**
	 * The regular expression used to test for denied requests.
	 */
	protected volatile Pattern deny = null;
	/**
	 * The current deny configuration value that may or may not compile into a
	 * valid {@link Pattern}.
	 */
	protected volatile String denyValue = null;
	/**
	 * Helper variable to catch configuration errors.
	 * It is <code>true</code> by default, but becomes <code>false</code>
	 * if there was an attempt to assign an invalid value to the
	 * <code>deny</code> pattern.
	 */
	protected volatile boolean denyValid = true;
	/**
	 * The HTTP response status code that is used when rejecting denied
	 * request. It is 403 by default, but may be changed to be 404.
	 */
	protected int denyStatus = HttpServletResponse.SC_FORBIDDEN;
	/**
	 * <p>If <code>invalidAuthenticationWhenDeny</code> is true
	 * and the context has <code>preemptiveAuthentication</code>
	 * set, set an invalid authorization header to trigger basic auth
	 * instead of denying the request..
	 */
	private boolean invalidAuthenticationWhenDeny = false;

	//------------------------------------------------------ Constructor
	public RequestFilterValve() {
		super(true);
	}

	// ------------------------------------------------------------- Properties

	/**
	 * Return the regular expression used to test for allowed requests for this
	 * Valve, if any; otherwise, return <code>null</code>.
	 */
	public String getAllow() {
		return allowValue;
	}

	/**
	 * Set the regular expression used to test for allowed requests for this
	 * Valve, if any.
	 *
	 * @param allow The new allow expression
	 */
	public void setAllow(String allow) {
		if (allow == null || allow.length() == 0) {
			this.allow = null;
			allowValue = null;
			allowValid = true;
		} else {
			boolean success = false;
			try {
				allowValue = allow;
				this.allow = Pattern.compile(allow);
				success = true;
			} finally {
				allowValid = success;
			}
		}
	}

	/**
	 * Return the regular expression used to test for denied requests for this
	 * Valve, if any; otherwise, return <code>null</code>.
	 */
	public String getDeny() {
		return denyValue;
	}

	/**
	 * Set the regular expression used to test for denied requests for this
	 * Valve, if any.
	 *
	 * @param deny The new deny expression
	 */
	public void setDeny(String deny) {
		if (deny == null || deny.length() == 0) {
			this.deny = null;
			denyValue = null;
			denyValid = true;
		} else {
			boolean success = false;
			try {
				denyValue = deny;
				this.deny = Pattern.compile(deny);
				success = true;
			} finally {
				denyValid = success;
			}
		}
	}

	/**
	 * Returns {@code false} if the last change to the {@code allow} pattern did
	 * not apply successfully. E.g. if the pattern is syntactically
	 * invalid.
	 */
	public final boolean isAllowValid() {
		return allowValid;
	}

	/**
	 * Returns {@code false} if the last change to the {@code deny} pattern did
	 * not apply successfully. E.g. if the pattern is syntactically
	 * invalid.
	 */
	public final boolean isDenyValid() {
		return denyValid;
	}

	/**
	 * Return response status code that is used to reject denied request.
	 */
	public int getDenyStatus() {
		return denyStatus;
	}

	/**
	 * Set response status code that is used to reject denied request.
	 */
	public void setDenyStatus(int denyStatus) {
		this.denyStatus = denyStatus;
	}

	/**
	 * Return descriptive information about this Valve implementation.
	 */
	@Override
	public String getInfo() {

		return (info);

	}

	/**
	 * Return true if a deny is handled by setting an invalid auth header.
	 */
	public boolean getInvalidAuthenticationWhenDeny() {
		return invalidAuthenticationWhenDeny;
	}

	/**
	 * Set invalidAuthenticationWhenDeny property.
	 */
	public void setInvalidAuthenticationWhenDeny(boolean value) {
		invalidAuthenticationWhenDeny = value;
	}


	// --------------------------------------------------------- Public Methods

	/**
	 * Extract the desired request property, and pass it (along with the
	 * specified request and response objects) to the protected
	 * <code>process()</code> method to perform the actual filtering.
	 * This method must be implemented by a concrete subclass.
	 *
	 * @param request  The servlet request to be processed
	 * @param response The servlet response to be created
	 * @throws IOException      if an input/output error occurs
	 * @throws ServletException if a servlet error occurs
	 */
	@Override
	public abstract void invoke(Request request, Response response)
			throws IOException, ServletException;


	// ------------------------------------------------------ Protected Methods

	@Override
	protected void initInternal() throws LifecycleException {
		super.initInternal();
		if (!allowValid || !denyValid) {
			throw new LifecycleException(
					sm.getString("requestFilterValve.configInvalid"));
		}
	}

	@Override
	protected synchronized void startInternal() throws LifecycleException {
		if (!allowValid || !denyValid) {
			throw new LifecycleException(
					sm.getString("requestFilterValve.configInvalid"));
		}
		super.startInternal();
	}

	/**
	 * Perform the filtering that has been configured for this Valve, matching
	 * against the specified request property.
	 *
	 * @param property The request property on which to filter
	 * @param request  The servlet request to be processed
	 * @param response The servlet response to be processed
	 * @throws IOException      if an input/output error occurs
	 * @throws ServletException if a servlet error occurs
	 */
	protected void process(String property, Request request, Response response)
			throws IOException, ServletException {

		if (isAllowed(property)) {
			getNext().invoke(request, response);
			return;
		}

		if (getLog().isDebugEnabled()) {
			getLog().debug(sm.getString("requestFilterValve.deny",
					request.getRequestURI(), property));
		}

		// Deny this request
		denyRequest(request, response);
	}

	protected abstract Log getLog();

	/**
	 * Reject the request that was denied by this valve.
	 * <p>If <code>invalidAuthenticationWhenDeny</code> is true
	 * and the context has <code>preemptiveAuthentication</code>
	 * set, set an invalid authorization header to trigger basic auth.
	 *
	 * @param request  The servlet request to be processed
	 * @param response The servlet response to be processed
	 * @throws IOException      if an input/output error occurs
	 * @throws ServletException if a servlet error occurs
	 */
	protected void denyRequest(Request request, Response response)
			throws IOException, ServletException {
		if (invalidAuthenticationWhenDeny) {
			Context context = request.getContext();
			if (context != null && context.getPreemptiveAuthentication()) {
				if (request.getCoyoteRequest().getMimeHeaders().getValue("authorization") == null) {
					request.getCoyoteRequest().getMimeHeaders().addValue("authorization").setString("invalid");
				}
				getNext().invoke(request, response);
				return;
			}
		}
		response.sendError(denyStatus);
	}

	/**
	 * Perform the test implemented by this Valve, matching against the
	 * specified request property value. This method is public so that it can be
	 * called through JMX, e.g. to test whether certain IP address is allowed or
	 * denied by the valve configuration.
	 *
	 * @param property The request property value on which to filter
	 */
	public boolean isAllowed(String property) {
		// Use local copies for thread safety
		Pattern deny = this.deny;
		Pattern allow = this.allow;

		// Check the deny patterns, if any
		if (deny != null && deny.matcher(property).matches()) {
			return false;
		}

		// Check the allow patterns, if any
		if (allow != null && allow.matcher(property).matches()) {
			return true;
		}

		// Allow if denies specified but not allows
		if (deny != null && allow == null) {
			return true;
		}

		// Deny this request
		return false;
	}
}
